/*
 * Copyright 2010
 * Jairou Software Organization
 * All Rights Reserved.
 * 
 * This software is the proprietary 
 * information of Roger Ramia
 * and the Jairou Software Organization 
 * Use is subject to license terms.
 * 
 * For licensing information please visit
 * http://www.jairou.org/license
 * 
 * @author Roger Ramia
 */
package org.jairou.web;

import java.sql.SQLException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.jairou.core.Exceptions;
import org.jairou.core.Objects;
import org.jairou.core.Strings;
import org.jairou.core.XML;
import org.jairou.util.DB;
import org.jairou.util.Upload;
import org.jairou.util.JS;
import org.jairou.util.Security;

/**
* Common utility providing core Http support
* for Servlets and other Web Container components
* <br/>
* This class provides delegation routines for 
* the Jairou Servlets, Filters and Listeners defined
* within the org.jairou.web package
* <br/>
* Although not specifically controlled by this class, it
* provides the underlying utilities necessary to handle 
* all xml configured requests typically defined in request.xml 
* <br/>
* @see org.jairou.web.JairouFilter and supporting documentation
* for more information about Web Request processing
* 
* @author Roger Ramia
*/
public class Web {	

	public static final String CONTEXT_KEY = "jairouContext";
	public static final String CLIENT_SCOPE_KEY = "client";
	public static final String REQUEST_SCOPE_KEY = "request";
	public static final String SESSION_SCOPE_KEY = "session";
	public static final String APPLICATION_SCOPE_KEY = "application";
	public static final String ERRORS_KEY = Exceptions.ERRORS_KEY;
	
	
	/**
	 * Parses out the relative path of the request
	 * stripping the server context path info.
	 * Thus a request for 
	 * 'http://localhost:8080/myWar/Person.jsp'
	 * will return simply 'Person.jsp'
	 *  
	 * @param request - current servlet request object
	 * @return - String representing the relative path
	 */
	public static String getPath(HttpServletRequest request) {
		String path = null;
		if (request != null) {
			String uri = request.getRequestURI();
			String ctxPath = request.getContextPath();
			path = uri.substring(uri.lastIndexOf(ctxPath)+ctxPath.length());
		}
		return path;
	}

	/**
	 * Parses out the relative path of the request
	 * stripping the server context path info and
	 * dropping any file extension info.
	 * Thus a request for 
	 * 'http://localhost:8080/myWar/Person.jsp'
	 * will return simply 'Person'
	 * 
	 * @param request - current servlet request object
	 * @return - String representing the id of the request
	 */
	public static String getId(HttpServletRequest request) {
		String id = null;
		String path = getPath(request);
		if (!Strings.nullOrEmpty(path)) {
			if (path.indexOf(".") != -1) id = path.substring(path.lastIndexOf("/")+1, path.lastIndexOf("."));
			else id = path.substring(path.lastIndexOf("/")+1);
		}
		return id;				
	}
	
	/**
	 * checks to see if an xml configuration exsist for the 
	 * specified id
	 * 
	 * @param id - id of the request in question
	 * @return - true if the request is defined in xml
	 */
	public static boolean isDefined(String id) {
		return (XML.contains("request."+id+".data.load") ||
				XML.contains("request."+id+".validation.field") ||
				XML.contains("request."+id+".secure") || 
				XML.contains("request."+id+".forward"));
	}
	
	/**
	 * overloaded method which checks to see if an xml 
	 * configuration exists for the id of the specified request
	 * 
	 * @param request - current servlet request object
	 * @return - true if the request is defined in xml
	 */
	public static boolean isDefined(HttpServletRequest request) {
		return isDefined(getId(request));
	}
	
	
	/**
	 * Converts a Servlet request object into a Map by
	 * enumerating the Parameters and Attributes
	 * and adding each to a HashMap.  A reference to 
	 * the active scope objects HttpServletRequest, HttpSession, and ServletContext
	 * are also placed into the Map keyed by 
	 * 'request', 'session' and 'application' respectively.
	 * Additionally a Map keyed 'client' is placed as a place holder for 
	 * data that should be consumed by the client.
	 * 
	 * @param req - The current Servlet Request to convert
	 * @return - Map with all input parameters/attributes and pointers to 
	 * 			 'request', 'session', 'application' and 'client' scope
	 */
	public static Map<String, Object> toMap(HttpServletRequest req) 
	throws Exception {
		Map<String, Object> out = null;
		if (req != null) {
			out = new HashMap<String, Object>();

			// adds request and remote info to ctx
			out.put("request", req);
			out.put("request_remote_address", req.getRemoteAddr());
			out.put("request_remote_host", req.getRemoteHost());
			out.put("request_remote_port", req.getRemotePort());
			out.put("request_remote_user", req.getRemoteUser());
			
			out.put("session", req.getSession());
			out.put("application", req.getSession().getServletContext());
			out.put("client", new HashMap());
            
            if (Upload.isMultipart(req)) Upload.addItems(out);
            else {      
				// add the parameters
				Enumeration<String> params = req.getParameterNames();
				if (params != null) {
					while (params.hasMoreElements()) {
						String name = params.nextElement();
						String val = req.getParameter(name);
						if (Strings.nullOrEmpty(val)) val = null;
						out.put(name, val);
					}
				}
				
				Enumeration<String> attrs = req.getAttributeNames();
				if (attrs != null) {
					while (attrs.hasMoreElements()) {
						String name = attrs.nextElement();
						out.put(name, req.getAttribute(name));
					}
				}				
            }
		}
		return out;
	}
	
	/**
	 * Searches for the current context within provided HttpServletRequest
	 * (stored as an attribute) and returns it.  If no context 
	 * exists calls the toMap() method to convert the 
	 * current request object into a context Map.  Finally
	 * sets it as the current context for subsequent calls.
	 * <br/>
	 * This call is thus guaranteed to return the 'current' context
	 * for any given http request.
	 * 
	 * @param request
	 * @return
	 */
	public static Object getContext(HttpServletRequest request) 
	throws Exception {
		Object ctx = null;
		if (request != null) {
			ctx = request.getAttribute(CONTEXT_KEY);
			if (ctx == null) {
				ctx = toMap(request);
				request.setAttribute(CONTEXT_KEY, ctx);
			}
		}
		return ctx;
	}
	
	/**
	 * This is the default web request handler implementation.
	 * It reads the request node for the specified id and executes
	 * the tasks defined within.  
	 * <br/>
	 * A standard request is configured using the following:
	 * <pre>{@code
	 * <request id="updateEmail" secure="true" permission="MANAGE_PEOPLE">		
	 *	<validation>
	 *		<field id="personId">
	 *			<rule id="required" message="Person Id is required"/>
	 *		</field>
	 *		<field id="emailAddress">			
	 *			<rule id="required" message="Email Address is required"/>
	 *			<rule id="email" message="Email format must be valid"/>
	 *		</field>
	 *	</validation>
	 *	<data transaction="true" connection="dbConn">
	 *		<load id="person" value="${update('savePerson')}"/>
	 * 	</data>
	 * </request>
	 * }</pre>
	 * 
	 * The request is handled by performing the following tasks:
	 * <pre>
	 * 	Web.checkSecurity() - checks authentication and authorization
	 * 	Web.validateFields() - validates inputs
	 * 	Web.loadData() - loads data
	 * </pre>
	 * upon completion the handler will forward the request to the 
	 * specified uri (or original uri if no forward is specified);
	 * <br/>
	 * To overwrite the standard handler behavior simple define a static 
	 * method on any Object and register it as a function in the jairou.xml 
	 * functions element. You must specify the id of your 
	 * custom handler like so:
	 * <pre>{@code
	 * <request handler="myCustomHandler">.  
	 * }</pre>
	 * Your handler implementation must accept a string identifying 
	 * the request, returned by Web.getId(), and an Object representing 
	 * the current request context(usually a Map). The id and context will
	 * be created and passed into your handler via the JairouFilter.  
	 * Finally your handler must return a uri to forward to after
	 * it completes
	 * 
	 * @param id - id of the request obtained by the JairouFilter by calling Web.getId()
	 * @param ctx - Current request context Map obtained by the JairouFilter by calling Web.getContext()
	 * @return - string indicating the url to forward the request to.
	 */
	public static String handle(String id, Object ctx) {
		String forward = null;
		if (id != null) {	
			forward = XML.get("request."+id+".forward");
			try {
				checkSecurity(id, ctx);
				validateFields(id, ctx);
				loadData("request."+id+".data", ctx);
			}
			catch(Throwable t) {
				forward = XML.get("request."+id+".failure");				
				Exceptions.addError(t, ctx);

				HttpServletRequest request = (HttpServletRequest) Objects.get("request", ctx);
				if (request != null) {
					Enumeration<String> params = request.getParameterNames();
					if (params != null) {
						while (params.hasMoreElements()) {
							String name = params.nextElement();
							request.setAttribute(name, request.getParameter(name));
						}
					}
					request.setAttribute(ERRORS_KEY, Exceptions.getErrors(ctx));					
				}
			}
		}
		return forward;
	}
	
	
	
	/**
	 * Given a request id checks to see if
	 * security is required. If so, checks authentication and authorization
	 * if fails throws an exception indicating the failure.
	 * <br/>
	 * Authentication is performed if the security='true' property is set
	 * on the request element xml.
	 * <br/>
	 * Authorization is performed if the permission='SOME_PERMISSION' property is set
	 * on the request element xml.
	 * 
	 * @param id - id of the request in question
	 * @param ctx - current request context
	 */
	public static void checkSecurity(String id, Object ctx) 
	throws Exception {
		if (id != null) {
			if ("true".equalsIgnoreCase(XML.get("request."+id+".secure"))) {
				if (!Security.authenticated(ctx)) {
					throw new Exception("Security Authentication Failed!");	
				}
				else {
					String perm = XML.get("request."+id+".permission");
					if (perm != null) {
						if (!Security.authorized(perm, ctx)) {							
							throw new SecurityException("Security Authorization Failed!");
						}
					}
				}
			}
		}
		else throw new Error("Unable to check security because no ID was specified for request.");
	}	
	
	
	/**
	 * Performs basic input field validation given an id to a request element
	 * containing a validation element in the following format:
	 * <pre>{@code 
	 * <request id="login">		
	 * 	<validation>
	 *		<field id="emailAddress">
	 *			<rule id="email" message="Email format must be valid"/>
	 *		</field>
	 *		<field id="password">
	 *			<rule id="required" message="Password is required"/>
	 *		</field>
	 *	</validation>
	 * </request>
	 * }</pre>
	 * 
	 * Validation is performed on every <field> id specified in the <validation> 
	 * element the id of the <rule> tag is assumed to be a pre-configured 
	 * Javascript function imported via the <script-engine> configuration
	 * in the jairou.xml.  A JS.checkRule() call is performed on the specified 
	 * rule passing the field value obtained from the current context
	 * as well as a pointer to the current context.  Validation is determined
	 * by the result of the underlying javascript function.  All validation
	 * errors are collected and added to the current context's errors object.  
	 * 
	 *
	 * @param id - of the request to be validated
	 * @param ctx - current context object
	 * @throws Exception - if any one field validation fails
	 */
	public static void validateFields(String id, Object ctx) 
	throws Exception {
		if (id != null) {
			boolean hasErrors = false;
			String validationPrefix = "request."+id+".validation";
			String[] fields = XML.getIds(validationPrefix+".field");
			for (String field : fields) {
				String[] rules = XML.getIds(validationPrefix+".field."+field+".rule");
				for (String rule : rules) {
					String value = (String)Objects.find(field, ctx);
					if (!JS.checkRule(rule, value, ctx)) {
						hasErrors = true;
						String msg = XML.get(validationPrefix+".field."+field+".rule."+rule+".message");
						if (msg == null) msg = field+" failed rule "+rule;
						Exceptions.addError(field, msg, ctx);
					}
				}
			}
			if (hasErrors) {
				throw new Exception("Field Validation Failed!");
			}			
		}
	}
	
	/**
	 * Performs data loading and updating given an id to a request element
	 * containing a data element in the following format: 
	 * <pre>{@code
	 * <request id="login">		
	 * 	<data transaction="true" connection="dbConn">
	 *		<load id="user" value="${js('login')}" scope="session"/>
	 *		<load id="client" value="${session.user}" scope="client"/>
	 *		<load id="event" value="${log('logged in')}"/>
	 *	</data>
	 * </request>
	 * }</pre>
	 * 
	 * Data loading is performed on every child <load> element defined in the <data> 
	 * element.  The id of the <load> tag is assigned to the resulting output
	 * of the value expression.  The resulting id and value are thus placed
	 * into the current context within the specified scope property.  If no 
	 * scope is specified, the data is simply set on the root level context
	 * object.  If the value expression returns null the specified id
	 * property is set to null on the specified scope object.
	 * <br/>
	 * The value expression can be a dot notation key denoting an existing
	 * object accessible via the context or a pre-configured function call
	 * defined in the <functions> tag in jairou.xml.  
	 * <br/>
	 * All DB function calls will be coordinated in a transaction across a 
	 * single connection if the transaction="true" connection="myConnection" 
	 * properties are set on the <data> node.
	 * 
	 * @param prefix - dot notation prefix of the xml node containing the load element
	 * @param ctx - current context to load data into
	 * @throws Exception - any underlying function call exception  
	 * 					   or reflection exception during data loading
	 */
	public static void loadData(String prefix, Object ctx) 
	throws Exception {

		if (prefix != null) {			
			boolean transaction = Boolean.parseBoolean(XML.get(prefix+".transaction"));
			try {
				if (transaction) {
					String connId = XML.get(prefix+".connection");			
					DB.connect(connId, ctx);			
					DB.getConnection(ctx).setAutoCommit(false);
				}
				String[] loads = XML.getIds(prefix+".load");
				if (loads != null && loads.length > 0) {
					for (String load: loads) {
						String value = XML.get(prefix+".load."+load);
						String scope = XML.get(prefix+".load."+load+".scope");
						Object context = ctx;
						if (scope != null) context = Objects.find(scope, ctx);
						Object val = XML.resolveExpression(value, ctx);
						if (context != null) {
							Objects.set(load, val, context);
						}
						else throw new Exception("unable to resove scope "+scope);	
					}
				}
				if (transaction) DB.commit(ctx);
			}
			catch(Exception e) {
				try {
					DB.rollback(ctx);
					throw new Exception(e);				
				}
				catch(SQLException sqle) {
					throw new Exception(sqle);
				}
			}
			finally {
				try {
					DB.close(ctx);
				}
				catch(SQLException sqle) {
					throw new Exception(sqle);
				}
			}
		}
		else throw new Error("Unable to load data because no ID was specified for request.");	
	}
	
	/**
	 * Convenience method to retrieve a scope object
	 * from the provided context
	 * 
	 * @param key - id of the scope object to be obtained (supports dot notation)
	 * @param ctx - current context
	 * @return - resulting object
	 */
	public static Object getScope(String key, Object ctx) {
		return Objects.find(key, ctx);
	}
	
	/**
	 * Convenience method to retrieve the current HttpRequest
	 * from the supplied context object
	 * @param ctx - current context object
	 * @return - HttpServletRequest object if found
	 */
	public static HttpServletRequest getRequest(Object ctx) {
		return (HttpServletRequest) getScope(REQUEST_SCOPE_KEY, ctx);
	}
	
	/**
	 * Convenience method to retrieve the current HttpSession
	 * from the supplied context object
	 * @param ctx - current context object
	 * @return - HttpSession object if found
	 */
	public static HttpSession getSession(Object ctx) {
		return (HttpSession) getScope(SESSION_SCOPE_KEY, ctx);
	}
	
	/**
	 * Convenience method to retrieve the current ServletContext
	 * from the supplied context object
	 * @param ctx - current context object
	 * @return - ServletContext object if found
	 */
	public static ServletContext getApplication(Object ctx) {
		return (ServletContext) getScope(APPLICATION_SCOPE_KEY, ctx);
	}
	
	/**
	 * Overloaded convenience method to retrieve the current ServletContext
	 * from the supplied context object
	 * @param ctx - current context object
	 * @return - ServletContext object if found
	 */
	public static ServletContext getApplication(HttpServletRequest request) 
	throws Exception {
		return (ServletContext) getScope(APPLICATION_SCOPE_KEY, getContext(request));
	}	
	
}